package hc.bre.cmn

import org.slf4j.Logger
import org.slf4j.LoggerFactory
import hc.bre.ComponentRegistry
import hc.bre.call.SqlQueryDefinition
import hc.bre.cmn.struct.DataStruct
import hc.bre.data.setting.SettingDefinition
import hc.bre.func.FunctionDefinition
import hc.bre.func.FunctionItem
import hc.bre.policy.CategoryDefinition
import hc.bre.policy.ReasonDefinition
import hc.bre.project.ProjectDefinition
import hc.bre.variable.VariableDefinition
import net.bytebuddy.asm.Advice.This

import java.nio.charset.Charset

@groovy.transform.CompileStatic
class RuleScriptBuilder<T extends RuleDefinition>{
	private static final Logger _log = LoggerFactory.getLogger(RuleScriptBuilder.class);

	T ruleDef;

	//single and multiple file mode for rule use different 'package' building method
	//projectPackage for single project script file
	String projectPackage;
	StringBuilder sbOfClass = new StringBuilder();
	int indent

	static enum ClassNameMode
	{
		FULL,SHORT
	}
	ClassNameMode classNameMode = ClassNameMode.FULL;
	HashMap<String,Class> classUsedMap = new HashMap<String,Class>();

	RuleScriptBuilder forRule(T ruleDef)
	{
		this.ruleDef = ruleDef;

		return this;
	}

	final public String packageName()
	{
		return projectPackage?:hc.Util.rpathPackageName(this.ruleDef.rpath);
	}

	final public String classShortName()
	{
		return hc.Util.rpathClassShortName(this.ruleDef.rpath);
	}

	final public String classFullName()
	{
		return hc.Util.rpathClassFullName(this.ruleDef.rpath);
	}

	String build()
	{
		//build class firstly, because the import instruction may come buildClass method
		buildClass();

		StringBuilder sb  = new StringBuilder();
		sb.append("package ${packageName()}");
		sb.append(TextOfImport());
		sb.append(sbOfClass);

		return sb.toString();
	}

	StringBuilder buildClass()
	{
		String extention = '';
		if(buildClass_extends() != Object.class)
		{
			extention = " extends " + className(buildClass_extends());
		};

		//The inner class declaration need 'static' keyword
		String staticFlag = '';
		if(packageName() != hc.Util.rpathPackageName(this.ruleDef.rpath))
		{
			staticFlag = 'static ';
		};

		apdNl("${staticFlag}public class ${classShortName()}${extention}{")

		nested(){
			buildClassMember();

			buildClassRule();
		};


		apdNl("};");

		return this.sbOfClass;
	}

	Class buildClass_extends()
	{
		return Object.class;
	}

	final void apdInnerClass(String definedClassName,Class extended=Object.class, Class implemented=null, Closure body) {
		if(extended ==null) extended = Object.class;

		String extension = '';
		if(extended != Object.class)
		{
			extension = " extends ${className(extended)} "
		}

		String implementation = '';
		if(implemented != null)
		{
			extension = " implements ${className(implemented)}"
		}

		apdNl("static public class ${definedClassName}${extension}${implementation}{");
		nested(body);
		apdNl("};")
	};

	void buildClassMember()
	{
		buildClassMemberClass();
		buildClassMemberVariable();
		buildClassMemberMethod();
	}

	void buildClassMemberClass() {

	};

	void buildClassMemberVariable() {

	};

	void buildClassMemberMethod() {

	}

	final RuleScriptBuilder apdClassVarDecl(String type, String code)
	{
		apdNl("${type} ${code};")//“public” keyword will prevent to automatically build prop accessor of field
	}

	final RuleScriptBuilder apdClassVarDecl(Class clazz, String code)
	{
		apdClassVarDecl(className(clazz),code)
	}
	final RuleScriptBuilder apdStaticFinalVar(String type, String code, String value)
	{
		apdNl("public static final ${type} ${code} = ${value};")
	}

	final RuleScriptBuilder apdStaticFinalVar(Class clazz, String code, String value)
	{
		apdStaticFinalVar(className(clazz),code,value)
	}

	void buildClassRule()
	{
		apdNl("public static Closure rule = {_context_ ->");

		indentInc();

		buildClasRuleBody();

		indentDec();

		apdNl("};");
	}
	void buildClasRuleBody()
	{
		buildClassRuleBodyResultInit();

		apdNl();
		buildClassRuleBodyCore();
		apdNl();

		buildClassRuleBodyResultReturn();
	};
	void buildClassRuleBodyResultInit()
	{
		String clazz = className(buildClassRuleBodyResult_Class());

		apdNl("${clazz} ${result} = new ${clazz}();");
//		apdNl("${result}.rid = ${ruleDef.rid};");
		apdNl("${result}.rname = ${referText(ruleDef.rname)};");
		apdNl("${result}.rpath = ${referText(ruleDef.rpath)};");
	}

	Class buildClassRuleBodyResult_Class()
	{
		return HashMap.class;
	}

	void buildClassRuleBodyCore()
	{

	}

	void buildClassRuleBodyResultReturn()
	{
		apdNl("return ${result};");
	}

//	final <RT extends RuleDefinition> RT rule(int id)
//	{
//		ProjectDefinition prj = project();
//
//		RT temp =  (RT)prj.ruleIdMap.get(id);
//
//		if(temp == null)
//		{
//			_log.error("canot find the rule definition in project for rule id {}",id)
//		};
//
//		return temp;
//	}

	final <RT extends RuleDefinition> RT rule(String rpath)
	{
		ProjectDefinition prj = project();

		RT temp =   (RT)prj.rulePathMap.get(rpath);

		if(temp == null)
		{
			_log.error("canot find the rule definition in project for rule path {}",rpath)
		};

		return temp;
	}

	final <RT extends RuleDefinition> RT rule(Class<RT> clazz)
	{
		String rtype = ComponentRegistry.get(clazz).rtype;

		ProjectDefinition prj = project();

		List<RuleDefinition> finded = prj.ruleList.findAll { it.rtype == rtype };

		if(finded.size() == 0 )
		{
			_log.error("The rule of ${clazz.getName()} is not found in project ${prj.rpath}");
			return null;

		};

		if(finded.size()>1)
		{
			_log.error("The rule of ${clazz.getName()} is not found in project ${prj.rpath}")
		}

		return (RT)finded.get(0);
	}


	//TODO RENAME TO ItemOfRule
	final<IT extends IListOption> String routeOfRuleItem(RuleDefinition ruleDef, Integer itemId , String defaultVal='null')
	{
		if(!ruleDef || !itemId) return defaultVal;

		List<IT> lst = ((IDetailList<IT>)ruleDef).routeOf(itemId);

		return referRuleClass(ruleDef) +"." + lst.collect({it.code}).join('.');
	}

	final SettingDefinition getRuleOfSetting()
	{
		return this.<SettingDefinition> rule(SettingDefinition.class)
	}

	final VariableDefinition getRuleOfVariable()
	{
		return this.<VariableDefinition> rule(VariableDefinition.class)
	}

	final CategoryDefinition getRuleOfCategory()
	{
		return this.<CategoryDefinition> rule(CategoryDefinition.class)
	}

	final ReasonDefinition getRuleOfReason()
	{
		return this.<ReasonDefinition> rule(ReasonDefinition.class)
	}

	final FunctionDefinition getRuleOfFunction()
	{
		return this.rule(FunctionDefinition.class)
	}

	final ProjectDefinition project()
	{
		return ruleDef.project;
	}

	final RuleScriptBuilder classUse(Class clazz)
	{
		classUsedMap.put(clazz.getName(), clazz);
		return this;
	}

	final String className(Class clazz)
	{
		classUse(clazz);

		if(classNameMode == ClassNameMode.SHORT || defaultImport(clazz))
		{
			return clazz.getSimpleName();
		}

		return clazz.getName();
	}

	/*
	 Default imports are the imports that Groovy language provides by default.
	 The below imports are added by groovy for you:
	 import java.lang.*
	 import java.util.*
	 import java.io.*
	 import java.net.*
	 import groovy.lang.*
	 import groovy.util.*
	 import java.math.BigInteger
	 import java.math.BigDecimal
	 */

	final static String[] _defaultPackageList = ['java.lang', 'java.util', 'java.io', 'java.net', 'groovy.lang', 'groovy.util'];
	final static String[] _defaultClassList =['java.math.BigInteger', 'java.math.BigDecimal'];

	final boolean defaultImport(Class clazz)
	{
		String packageName = clazz.getPackage().getName();//complaint with 1.8. getPackageName();
		for(int i=0;i<_defaultPackageList.size();i++)
		{
			if(_defaultPackageList[i] == packageName) return true;
		}

		String className = clazz.getName();
		for(int i=0;i<_defaultClassList.size();i++)
		{
			if(_defaultClassList[i] == className) return true;
		}

		return false;
	}

	final StringBuilder TextOfImport()
	{
		List<String> lst = [];

		StringBuilder sb = new StringBuilder();

		if(classNameMode == ClassNameMode.FULL)
		{
			return sb
		};

		classUsedMap.each { k,v ->
			if(defaultImport(v)) return;//ignore the default import of groovy;

			lst.add("\nimport " + k);
		};

		lst = lst.sort();

		lst.each{
			sb.append(it);
		};

		return sb;
	}

	final String referVariable(Integer id)
	{
		if(!id) return 'null';
		return this.ruleOfVariable.accessRouteOf(id);
	}

//	final String referApplyRule(int rid)
//	{
//		return  referApplyRule(rule(rid));
//	}

//	final String referApplyRule(String rpath)
//	{
//		return  referApplyRule(rule(rpath));
//	}
	
	final String referApplyRule(String rpath)
	{
		return "apply(${referRuleClass(rpath)}.rule)";
	}

//	final String referApplyRule(RuleDefinition rule)
//	{
//		//TODO deal with
//		/* # from rid to the rule path
//		 * # long or short reference of rule
//		 * # delegate | this
//		 * # apply | with
//		 */
//		return "apply(${referRuleClass(rule)}.rule)";
//	}
	
	final String referApplyRule(RuleDefinition rule)
	{
		referApplyRule(rule.rpath)
	}

//	final String referRuleClass(int rid)
//	{
//		RuleDefinition rule = this.rule(rid);
//		return referRuleClass(rule);
//	}

	//	final String referRuleClass(String rpath)
	//	{
	//		RuleDefinition rule = this.rule(rpath);
	//		return referRuleClass(rule);
	//	}
	//
	//	final String referRuleClass(RuleDefinition rule)
	//	{
	//		String name =  rule.rpath.replace('/', '.');
	//
	//		if(name.startsWith(packageName()))
	//		{
	//			name = name.substring(packageName().length()+1);
	//		};
	//		return name;
	//	}

	final String referRuleClass(String rpath)
	{
		String name =  rpath.replace('/', '.');

		if(name.startsWith(packageName()))
		{
			name = name.substring(packageName().length()+1);
		};
		return name;
	}

	final String referRuleClass(RuleDefinition rule)
	{
		referRuleClass(rule.rpath)
	}

	final String referRuleClass()
	{
		referRuleClass(this.ruleDef)
	}

	final String referNumber(Object input)
	{
		if(input == null || input == '') return 'null';
		return input.toString();
	}

	final String referText(Object input)
	{
		if(input == null) return 'null';
		return "'${hc.Util.esc(input.toString())}'";
	}

	final String referData(Object input,String dataType)
	{
		String value;
		if(dataType == 'number') {
			value = referNumber(input);
		}
		else if(dataType == 'text') {
			value = referText(input);
		}
		else if(dataType == 'object'){
			//TODO improve the logic
			value = referNumber(input);
		}else
		{
			throw new Exception ("Not Supported data type.");
		};

		return value;
	}

//	final String referResultItem(int rid,int itemId)
//	{
//		return resultStruct(rid).accessRouteOf(itemId);
//	}

//	final DataStruct resultStruct(int rid)
//	{
//		RuleDefinition ruleDef = this.rule(rid);
//		DataStruct resultStruct = ComponentRegistry.componentMap.get(ruleDef.rtype).resultStruct;
//		return resultStruct;
//	}

	final DataStruct resultStruct(String rpath)
	{
		RuleDefinition ruleDef = this.rule(rpath);
		DataStruct resultStruct = ComponentRegistry.componentMap.get(ruleDef.rtype).resultStruct;
		return resultStruct;
	}

	final String getResult()
	{
		return '_result_';
	}

	final String getRresult()
	{
		return DataStruct.rresultToken;
	}

	final RuleScriptBuilder apd(String text)
	{
		sbOfClass.append(text);
		return this;
	}

	final RuleScriptBuilder apd(StringBuilder sb)
	{
		sbOfClass.append(sb);
		return this;
	}

	final RuleScriptBuilder apdNl(String text)
	{
		sbOfClass.append(getNl()).append(text);
		return this;
	}

	final RuleScriptBuilder apdNl(StringBuilder sb)
	{
		sbOfClass.append(getNl()).append(sb);
		return this;
	}

	final RuleScriptBuilder apdNlBlock(String text) {
		if(text == null) return this;
		text = text.trim();

		BufferedReader br = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(text.getBytes(Charset.forName("utf8"))), Charset.forName("utf8")));
		String line;

		while ( (line = br.readLine()) != null ) {
			apdNl(line);
		}

		return this;
	}

	final RuleScriptBuilder apdNl() {
		sbOfClass.append(getNl());
		return this;
	}

	final RuleScriptBuilder insert(String text)
	{
		sbOfClass.insert(0,text);
		return this;
	}

	final RuleScriptBuilder insertNl(String text)
	{
		sbOfClass.insert(0,text).insert(0,getNl());
		return this;
	}

	final RuleScriptBuilder insertNl() {
		sbOfClass.insert(0,getNl());
		return this;
	}

	final RuleScriptBuilder indent(int depth) {
		indent = depth;
		return this;
	}

	final RuleScriptBuilder indentInc() {
		++indent;
		return this;
	}

	final RuleScriptBuilder indentDec() {
		--indent;
		return this;
	}

	final void nested(Closure closure) {
		indentInc();

		closure.call();

		indentDec()
	}

	//access it with ${nl}
	final String getNl()
	{
		return getNewLine();
	}

	//access it with ${newLine}
	final String getNewLine() {
		StringBuilder sb = new StringBuilder();
		appendNewLine(sb);
		return sb.toString();
	}

	final private void appendNewLine(StringBuilder sb) {
		sb.append('\n');
		for(int i = 0 ; i < indent;i ++) {
			sb.append('\t');
		}
	}
}
